/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

import * as fs from 'fs';

import { INCREMENTAL_INPUT_OUTPUT_CACHE } from '../../../../common/const/const.js';
import { HvigorCoreNode, Project } from '../../../external/core/hvigor-core-node.js';
import { hvigorCore } from '../../../external/core/hvigor-core.js';
import { CoreTaskImpl } from '../../../external/task/core-task-impl.js';
import { IncrementalExecTask } from '../../../external/task/incremental-exec-task.js';
import { IncrementalTask } from '../../../external/task/incremental-task.js';
import { HvigorLogger } from '../../../log/hvigor-log.js';
import { formatTime } from '../../../util/time-util.js';
import { ProjectCacheService } from '../../cache/project-cache-service.js';
import { getTaskSnapCacheEntryUniqueKey } from '../../cache/util/cache-util.js';
import { BasicFileSnapshot } from '../../snapshot/core/filesystem-snapshot.js';
import { TaskSnapshot } from '../../snapshot/core/task-snapshot.js';
import { SnapshotComparatorService } from '../../snapshot/service/snapshot-comparator-service.js';
import { SnapshotGeneratorService } from '../../snapshot/service/snapshot-generator-service.js';
import { FileSet, FsOptions } from '../../snapshot/util/file-set.js';

const _log = HvigorLogger.getLogger('hvigor-task-proxy');

function withCache(fn: Function) {
  let cachedResult: any = null;
  let hasCached = false;

  return function (this: any, ...args: any[]) {
    if (hasCached) {
      return cachedResult;
    }
    cachedResult = fn.apply(this, ...args);
    hasCached = true;
    return cachedResult;
  };
}

/**
 * 增量Task的一个中间代理类，用于在任务实际执行前进行一些增量检查,执行后,刷新增量缓存之类的
 *
 * @since 2022/9/1
 */
export class TaskProxy {
  private readonly project: Project;
  private readonly task: IncrementalTask;
  private readonly taskNode: HvigorCoreNode;
  private readonly cacheTaskSnapShot: TaskSnapshot | undefined;

  private projectCacheService: ProjectCacheService;
  private snapshotGeneratorService: SnapshotGeneratorService;
  private snapshotComparatorService: SnapshotComparatorService;

  private curTaskSnapShot: TaskSnapshot | undefined;
  private curInputFileSnapShotMap: Map<string, BasicFileSnapshot> | undefined;

  private readonly notDeclareAnyIncrementalLogic: boolean = false;

  private readonly useInputOutputCache = false;

  constructor(task: CoreTaskImpl) {
    this.task = task as IncrementalTask;
    this.taskNode = task.getNode();
    this.project = this.taskNode.getProject();
    this.projectCacheService = ProjectCacheService.getInstance(this.project);

    // 是否启用 declareInputs declareInputFiles declareOutputFiles 方法缓存
    this.useInputOutputCache = hvigorCore.getParameter().getProperty(INCREMENTAL_INPUT_OUTPUT_CACHE) ?? false;
    if (this.useInputOutputCache) {
      // 对输入做缓存
      this.task.declareInputs = withCache(this.task.declareInputs);
      this.task.declareInputFiles = withCache(this.task.declareInputFiles);
      // 对输出做缓存
      this.task.declareOutputFiles = withCache(this.task.declareOutputFiles);
    }

    this.snapshotGeneratorService = SnapshotGeneratorService.getInstance();
    this.snapshotComparatorService = SnapshotComparatorService.getInstance();

    this.cacheTaskSnapShot = this.projectCacheService.getTaskSnapShot(task.getName(), this.taskNode);
    this.curInputFileSnapShotMap = new Map<string, BasicFileSnapshot>();

    if (this.useInputOutputCache) {
      this.notDeclareAnyIncrementalLogic =
        this.task.declareInputs().size === 0 && this.task.declareInputFiles().collect().size === 0 && this.task.declareOutputFiles().collect().size === 0;
      if (this.notDeclareAnyIncrementalLogic && task instanceof IncrementalExecTask) {
        this.notDeclareAnyIncrementalLogic =
          task.declareExecutionTool() === '' && task.declareExecutionCommand() === '' && task.declareExecutionEnv().size === 0;
      }
    }
  }

  /**
   * 判断单个Task是否发生变化
   *
   * @return {boolean}
   * @private
   */
  private isTaskChange(): boolean {
    this.curTaskSnapShot!.updateCacheFilesOptions(this.projectCacheService);

    // 之前没有增量缓存,新增的增量任务
    if (this.cacheTaskSnapShot === undefined || !this.cacheTaskSnapShot.isSuccessful()) {
      _log.debug(`${this.task.getPath()} is not up-to-date, since no history record is available.`);
      this.task.durationEvent.addTaskRunReason('No history record is available.');
      return true;
    }

    // 任务快照是否相同
    if (SnapshotComparatorService.isDiff(this.cacheTaskSnapShot, this.curTaskSnapShot)) {
      _log.debug(`${this.task.getPath()} is not up-to-date, since the task snapshots are different.`);
      this.task.durationEvent.addTaskRunReason('The task snapshots are different.');
      return true;
    } else {
      return false;
    }
  }

  /**
   * 判断单个文件是否发生变化
   *
   * @param {string} filePath
   * @param options
   * @return {[BasicFileSnapshot, boolean]}
   * @private
   */
  private getFileCompareResult(filePath: string, options?: FsOptions): [BasicFileSnapshot, boolean] {
    const curFileSnapShot = this.snapshotGeneratorService.generateFileSystemSnapshot(filePath, options);

    const cacheFileSnapShot = this.projectCacheService.getFileSnapShot(filePath);

    if (!cacheFileSnapShot) {
      return [curFileSnapShot!, true];
    }

    return [curFileSnapShot!, SnapshotComparatorService.isDiff(cacheFileSnapShot, curFileSnapShot!)];
  }

  /**
   * 判断任务设置的增量输入文件是否发生变化
   *
   * @return {boolean}
   * @private
   */
  private isTaskInputFilesChange(): boolean {
    const inputFiles: FileSet = this.task.declareInputFiles();

    const inputFileGroup: Map<string, FsOptions> = inputFiles.collect();
    for (const [inputFilePath, options] of inputFileGroup) {
      // 增量指定的任务输入文件必须存在
      if (!fs.existsSync(inputFilePath)) {
        throw new Error(`Failed to find the incremental input file: ${inputFilePath}.`);
      }
      const fileChangeResult = this.getFileCompareResult(inputFilePath, options);
      if (fileChangeResult[1]) {
        // 输入文件如果发生变化,直接将生成的snapshot暂时缓存,任务执行完成后刷新缓存
        this.curInputFileSnapShotMap?.set(inputFilePath, fileChangeResult[0]);
        _log.debug(`${this.task.getPath()} is not up-to-date, since the input file '${inputFilePath}' ` + 'has been changed.');
        this.task.durationEvent.addTaskRunReason(`The input file '${inputFilePath}' has been changed.`);
        return true;
      }
    }

    return false;
  }

  /**
   * 判断任务设置的增量输出文件是否发生变化
   *
   * @return {boolean}
   * @private
   */
  private isTaskOutputFilesChange(): boolean {
    const outputFiles: FileSet = this.task.declareOutputFiles();

    const outputFileGroup: Map<string, FsOptions> = outputFiles.collect();
    for (const [outputFilePath, options] of outputFileGroup) {
      // 输出文件不存在
      if (!fs.existsSync(outputFilePath)) {
        _log.debug(`${this.task.getPath()} is not up-to-date, since the output file '${outputFilePath}' ` + 'does not exist.');
        this.task.durationEvent.addTaskRunReason(`The output file '${outputFilePath}' does not exist.`);
        return true;
      }

      // 输出文件发生改变
      if (this.getFileCompareResult(outputFilePath, options)[1]) {
        _log.debug(`${this.task.getPath()} is not up-to-date, since the output file '${outputFilePath}' ` + 'has been changed.');
        this.task.durationEvent.addTaskRunReason(`The output file '${outputFilePath}' has been changed.`);
        return true;
      }
    }

    return false;
  }

  /**
   * 执行任务之前的前置任务，比如增量的检查
   *
   * @return {boolean} true/false true->不需要重新执行,false->需要重新执行
   */
  preExecute(): boolean {
    const preExecuteStartTime: [number, number] = process.hrtime();
    if (this.useInputOutputCache) {
      if (this.notDeclareAnyIncrementalLogic) {
        // 如果一个Incremental task 未定义任何输入输出，则每次都要重新执行
        const preExecuteEndTime: [number, number] = process.hrtime(preExecuteStartTime);
        const realTime = formatTime(preExecuteEndTime);
        _log.debug(`${this.task.getPath()} is declared as IncrementTask, but does not implement any incremental method.`);
        _log.debug(`Incremental task ${this.task.getPath()} pre-execution cost: ${realTime}.`);
        return false;
      }
    }

    // 必须保证这个taskSnapShot被创建出来,保证后续task的执行状态能正常的被更新
    this.curTaskSnapShot = this.snapshotGeneratorService.generateTaskSnapshot(this.task);

    // 判断之前task有没有成功,没有成功直接执行
    if (!this.cacheTaskSnapShot?.isSuccessful()) {
      return false;
    }

    // 先校验task的所有输入文件
    const isTaskInputFilesChange = this.isTaskInputFilesChange();

    if (isTaskInputFilesChange) {
      const preExecuteEndTime: [number, number] = process.hrtime(preExecuteStartTime);
      const realTime = formatTime(preExecuteEndTime);
      _log.debug(`Incremental task ${this.task.getPath()} pre-execution cost: ${realTime}.`);
      return false;
    }

    // 然后校验task的所有输出文件
    const isTaskOutputFilesChange = this.isTaskOutputFilesChange();

    if (isTaskOutputFilesChange) {
      const preExecuteEndTime: [number, number] = process.hrtime(preExecuteStartTime);
      const realTime = formatTime(preExecuteEndTime);
      _log.debug(`Incremental task ${this.task.getPath()} pre-execution cost: ${realTime}.`);
      return false;
    }

    // 最后校验task本身
    const isTaskChange = this.isTaskChange();

    // 三者都未发生改变,则task不需要重新执行
    const compareResult = !isTaskChange && !isTaskInputFilesChange && !isTaskOutputFilesChange;

    const preExecuteEndTime: [number, number] = process.hrtime(preExecuteStartTime);
    const realTime = formatTime(preExecuteEndTime);

    _log.debug(`Incremental task ${this.task.getPath()} pre-execution cost: ${realTime}.`);
    return compareResult;
  }

  /**
   * 执行任务的实际TaskAction逻辑
   */
  async execute() {
    await this.task.execute();
  }

  /**
   * 任务执行成功后的刷新缓存
   */
  postExecute() {
    const postExecuteStartTime: [number, number] = process.hrtime();

    // 任务如果是增量结束的则不需要执行任何操作
    if (this.task.taskExecutedStatus.isUpToDate()) {
      _log.debug(`There is no need to refresh cache, since the incremental task ${this.task.getPath()} is up-to-date.`);
      return;
    }

    // 刷新输入文件的SnapShot
    this.updateTaskInputFilesSnapshot();

    // 任务执行后,则所有输出文件的SnapShot都需要重新生成并刷新
    this.updateTaskOutputFilesSnapShot();

    // 刷新TaskSnapShot
    this.updateTaskSnapShot();

    const postExecuteEndTime: [number, number] = process.hrtime(postExecuteStartTime);
    const realTime = formatTime(postExecuteEndTime);

    _log.debug(`Incremental task ${this.task.getPath()} post-execution cost:${realTime}.`);
  }

  private updateTaskOutputFilesSnapShot() {
    this.task
      .declareOutputFiles()
      .collect()
      .forEach((value, key) => {
        _log.debug(`Update task ${this.task.getPath()} output file:${key} cache.`);
        const lastOutputFileSnapShot = this.snapshotGeneratorService.generateFileSystemSnapshot(key, value);
        this.projectCacheService.updateFileSnapShot(key, lastOutputFileSnapShot!);
      });
  }

  private updateTaskInputFilesSnapshot() {
    this.task
      .declareInputFiles()
      .collect()
      .forEach((value, key) => {
        // 如果在任务执行前已经生成过，可以直接取并且更新缓存
        if (this.curInputFileSnapShotMap!.has(key)) {
          _log.debug(`Update task ${this.task.getPath()} input file:${key} cache from map.`);
          this.projectCacheService.updateFileSnapShot(key, this.curInputFileSnapShotMap!.get(key)!);
        } else {
          _log.debug(`Update task ${this.task.getPath()} input file:${key} cache by regenerate.`);
          const lastInputFileSnapShot = this.snapshotGeneratorService.generateFileSystemSnapshot(key, value);
          this.projectCacheService.updateFileSnapShot(key, lastInputFileSnapShot);
        }
      });
  }

  private updateTaskSnapShot() {
    const taskSnapShotCacheEntryUniqueKey = getTaskSnapCacheEntryUniqueKey(this.task.getName(), this.taskNode);

    // 刷新TaskSnapShot
    if (!this.curTaskSnapShot) {
      this.curTaskSnapShot = this.snapshotGeneratorService.generateTaskSnapshot(this.task);
    }

    // 最后保存对应文件的FileSnapShot到TaskSnapShot里
    this.curTaskSnapShot.updateFilesOptions(this.projectCacheService);

    this.projectCacheService.updateTaskSnapShot(taskSnapShotCacheEntryUniqueKey, this.curTaskSnapShot!);
  }

  getCurTaskSnapShot(): TaskSnapshot | undefined {
    return this.curTaskSnapShot;
  }
}
